package ds.safelock.db;

/**
 * This class provides an interface which allows us to handle our userdb
 * @author David Silva
 * @version 1.0
 */
public class UserDBHelper {

	
	/**
	 * Validates some user with his credentials
	 * @param user users username
	 * @param key users key
	 * @return the user object which contains the user data, null if invalid credentials were used
	 * 		   (null is most likely to happen only when a typo in username occurs, otherwise an exception
	 * 			should be throwed)
	 * @throws ds.safelock.exception.SaltNotFoundException if the user salt file doesn't exist
	 * @throws ds.safelock.exception.FileCorruptedException if the user auth/salt file is corrupted
	 * @throws ds.safelock.exception.InvalidUserException if the given username has invalid chars
	 * @throws ds.safelock.exception.UserNotFoundException if the given username does not exist
	 * @throws java.io.IOException if any errors occurs while reading any of the user files
	 */
	public static ds.safelock.User login(String user, String key) throws ds.safelock.exception.FileCorruptedException,
															   ds.safelock.exception.InvalidUserException,
															   ds.safelock.exception.UserNotFoundException,
															   ds.safelock.exception.SaltNotFoundException,
															   java.io.IOException
	{
		/** validate de user **/
		String cleanuser = cleanUsername(user);
		if (!validUser(cleanuser))
			throw new ds.safelock.exception.InvalidUserException("Invalid user detected (cleaned: "+cleanuser+")");
		String home = ds.safelock.config.Configuration.USER_DIR+cleanuser+"/";
		java.io.File dir = new java.io.File(home);
		if (!dir.exists())
		{
			dir = null;
			throw new ds.safelock.exception.UserNotFoundException();
		}

		/** get the user salt **/
		final byte[] salt = ds.safelock.security.Salt.getSalt(home, key.toCharArray()); //error with invalid key!
		
		/** get all the data from auth file **/
		final byte[] _iv, _data;
		byte[] _mac;
		try
		{
			final byte[] data = ds.safelock.io.FileHelper.read(home+ds.safelock.config.Configuration.FILE_AUTH);
			byte[] len = new byte[4];
			System.arraycopy(data, (data.length-len.length), len, 0, len.length);
			int m_len = ds.safelock.util.ByteHelper.byteArrayToInt(len);
			if (m_len <= 0 || m_len>=(data.length-4))
				throw new ds.safelock.exception.FileCorruptedException("Invalid mac len found!");
			java.util.Arrays.fill(len, (byte) 0x00);
			_mac = new byte[m_len];
			System.arraycopy(data, (data.length-len.length-_mac.length), _mac, 0, _mac.length);
			System.arraycopy(data, (data.length-len.length-_mac.length-len.length), len, 0, len.length);
			int iv_len = ds.safelock.util.ByteHelper.byteArrayToInt(len);
			if (iv_len <= 0 || m_len>=(data.length-4-m_len))
				throw new ds.safelock.exception.FileCorruptedException("Invalid iv len found!");
			java.util.Arrays.fill(len, (byte) 0x00);
			_iv = new byte[iv_len];
			_data = new byte[data.length-len.length-_mac.length-len.length-_iv.length];
			System.arraycopy(data, 0, _data, 0, _data.length);
			System.arraycopy(data, _data.length, _iv, 0, _iv.length);
			java.util.Arrays.fill(data, (byte) 0x00);
		}
		catch (ArrayIndexOutOfBoundsException e) { throw new ds.safelock.exception.FileCorruptedException("Invalid auth file found!"); }

		/** decipher the data **/
		byte[] content = ds.safelock.security.Cipher.decipher(_data, key.toCharArray(), salt, _iv);
		if (content==null)
			return null; //most likely to be an invalid key
		java.util.Arrays.fill(_iv, (byte) 0x00);
		java.util.Arrays.fill(_data, (byte) 0x00);

		/** check mac **/
		byte[] c_mac = ds.safelock.security.MAC.compute(content, key.toCharArray(), salt);
		if (!ds.safelock.security.MAC.compareMACs(_mac, c_mac))
			throw new ds.safelock.exception.FileCorruptedException("Invalid mac found!");
		java.util.Arrays.fill(c_mac, (byte) 0x00);
		java.util.Arrays.fill(_mac, (byte) 0x00);
		
		/** get the user data **/
		final String[] userdata = new String(content).split(":");
		if (userdata.length!=5)
			throw new ds.safelock.exception.FileCorruptedException("Invalid auth file found!");
		long ts;
		try { ts = Long.parseLong(userdata[1]); }
		catch (NumberFormatException e) { throw new ds.safelock.exception.FileCorruptedException("Invalid id found!"); }
		final String c_user = new String(ds.safelock.security.Base64.decode(userdata[2].getBytes()));
		final byte[] c_key = ds.safelock.security.Base64.decode(userdata[3].getBytes());
		final byte[] _key = ds.safelock.security.SHA.compute(key.getBytes());
		_mac = ds.safelock.security.Base64.decode(userdata[4].getBytes());
		if (_mac==null)
			throw new java.io.IOException("an error ocurred while encoding the key");
		
		/** check internal mac **/
		String tmp = ts+c_user+key;
		c_mac = ds.safelock.security.MAC.compute(tmp.getBytes(), key.toCharArray(), salt);
		if (!ds.safelock.security.MAC.compareMACs(_mac, c_mac))
			throw new ds.safelock.exception.FileCorruptedException("Invalid mac found!");
		java.util.Arrays.fill(salt, (byte) 0x00);
		java.util.Arrays.fill(_mac, (byte) 0x00);
		java.util.Arrays.fill(c_mac, (byte) 0x00);

		/** check login credentials (if we're here, it's valid for this file at least 
			 but let's do it anyway) **/
		ds.safelock.User u = null;
		if (c_user.equals(user) && java.util.Arrays.equals(c_key,_key))
			u = new ds.safelock.User(ts,c_user);
		java.util.Arrays.fill(_key, (byte) 0x00);
		java.util.Arrays.fill(c_key, (byte) 0x00);
		
		return u;
	}
	
	/**
	 * Register a given user in userdb
	 * @param user username
	 * @param key the key to be associated with given user
	 * @throws InvalidUserException if the given username has invalid chars
	 * @throws UserAlreadyRegisteredException if the given username is already taken
	 * @throws IOException if any kind of errors occurs while ciphering/computing/writing the needed data 
	 */
	public static void registerUser(String user, String key) throws ds.safelock.exception.InvalidUserException,
																	ds.safelock.exception.UserAlreadyRegisteredException,
																	java.io.IOException
	{
		/** validate de user **/
		String cleanuser = cleanUsername(user);
		if (!validUser(cleanuser))
			throw new ds.safelock.exception.InvalidUserException("Invalid user detected (cleaned: "+cleanuser+")");
		String home = ds.safelock.config.Configuration.USER_DIR+cleanuser+"/";
		java.io.File dir = new java.io.File(home);
		if (dir.exists())
		{
			dir = null;
			throw new ds.safelock.exception.UserAlreadyRegisteredException();
		}
		
		/** ok, lets create user dir **/
		dir.mkdirs();
		
		/** now we need to generate and store the user salt **/
		final byte[] salt = ds.safelock.security.Salt.generateSalt();
		ds.safelock.security.Salt.storeSalt(home, salt, key.toCharArray());		
		
		/** auth file content **/
		long ts = System.currentTimeMillis();
		String premac = ts+user+key;
		final byte[] _key = ds.safelock.security.SHA.compute(key.getBytes());
		if (_key==null)
			throw new java.io.IOException("an error ocurred while encoding the key");
		byte[] _mac = ds.safelock.security.MAC.compute(premac, key, salt);
		if (_mac==null)
			throw new java.io.IOException("an error ocurred while computing the mac [1]");
		String enc_user = new String(ds.safelock.security.Base64.encodeBytes(user.getBytes()));
		String enc_key = new String(ds.safelock.security.Base64.encodeBytes(_key));
		String mac = new String(ds.safelock.security.Base64.encodeBytes(_mac));
		java.util.Arrays.fill(_key, (byte) 0x00);
		java.util.Arrays.fill(_mac, (byte) 0x00);
		final String output = "u:"+ts+":"+enc_user+":"+enc_key+":"+mac;
		enc_user = enc_key = mac = null;
		
		/** cipher the entire stuff **/
		final byte[][] _e = ds.safelock.security.Cipher.cipher(output.getBytes(), key.toCharArray(), salt);
		if (_e==null)
			throw new java.io.IOException("an error ocurred while ciphering the data");

		/** MAC for the file content **/
		_mac = ds.safelock.security.MAC.compute(output, key, salt);
		java.util.Arrays.fill(salt, (byte) 0x00);
		if (_mac==null)
			throw new java.io.IOException("an error ocurred while computing the mac [2]");
		
		final byte[] m_len = ds.safelock.util.ByteHelper.intToByteArray(_mac.length), 
					iv_len = ds.safelock.util.ByteHelper.intToByteArray(_e[1].length);
		final byte[] data = new byte[_e[0].length+_e[1].length+iv_len.length+_mac.length+m_len.length];
		System.arraycopy(_e[0],  0, data, 0, _e[0].length);
		System.arraycopy(_e[1],  0, data, (_e[0].length), _e[1].length);
		System.arraycopy(iv_len, 0, data, (_e[0].length+_e[1].length), iv_len.length);
		System.arraycopy(_mac,   0, data, (_e[0].length+_e[1].length+iv_len.length), _mac.length);
		System.arraycopy(m_len,  0, data, (_e[0].length+_e[1].length+iv_len.length+_mac.length), m_len.length);
		java.util.Arrays.fill(m_len, (byte) 0x00);
		java.util.Arrays.fill(iv_len, (byte) 0x00);
		java.util.Arrays.fill(_mac, (byte) 0x00);
		
		/** store auth content **/
		ds.safelock.io.FileHelper.write(home+ds.safelock.config.Configuration.FILE_AUTH, data);
		java.util.Arrays.fill(data, (byte) 0x00);
		
	}
	
	private static boolean validUser(String username)
	{
		java.util.regex.Pattern p = java.util.regex.Pattern.compile("^[a-z]{1}[a-z0-9\\-\\.]");
		java.util.regex.Matcher m = p.matcher(username);
		return m.find();
	}
	
	private static String cleanUsername(String input)
	{
		String clean = input.toLowerCase();
		for (int i = 0; i < ds.safelock.config.Configuration.REPLACEMENT.length; i++)
			for (int j = 1; j < ds.safelock.config.Configuration.REPLACEMENT[i].length; j++)
				clean = clean.replaceAll(ds.safelock.config.Configuration.REPLACEMENT[i][j],
										 ds.safelock.config.Configuration.REPLACEMENT[i][0]);
		return clean;
	}
	
}
